package indi.mybatis.flying.handlers;

import indi.mybatis.flying.exception.SnowFlakeException;
import indi.mybatis.flying.type.KeyHandler;

public class SnowFlakeKeyHandler implements KeyHandler {

	private SnowFlakeKeyHandler() {
	}

	private static class InnerInstance {
		private static final SnowFlakeKeyHandler instance = new SnowFlakeKeyHandler(0, 0);
	}

	public static SnowFlakeKeyHandler getInstance() {
		return InnerInstance.instance;
	}

	private final long twepoch = 1420041600000L;

	/* The number of digits in the machine id. */
	private final long workerIdBits = 5L;

	/* The number of digits in the data id. */
	private final long datacenterIdBits = 5L;

	/*
	 * The maximum machine id supported, the result is 31 (this shift algorithm
	 * can quickly calculate the maximum decimal number that can be represented
	 * by several binary digits)
	 */
	private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

	/* The maximum supported data id id, the result is 31. */
	private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

	/* The number of digits in the id. */
	private final long sequenceBits = 12L;

	/* The machine ID moves to the left 12 places. */
	private final long workerIdShift = sequenceBits;

	/* The data id id is moved to the left 17 bits (12+5) */
	private final long datacenterIdShift = sequenceBits + workerIdBits;

	/* Time truncation to the left 22 bits (5+5+12) */
	private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

	/*
	 * The mask of the generated sequence, which is 4095
	 * (0b111111111111=0xfff=4095)
	 */
	private final long sequenceMask = -1L ^ (-1L << sequenceBits);

	/* Working machine ID(0~31) */
	private long workerId;

	/* Data center ID(0~31) */
	private long datacenterId;

	/* Sequence in milliseconds (0~4095) */
	private long sequence = 0L;

	/* The time stamp that generated the ID last time. */
	private long lastTimestamp = -1L;

	/**
	 * 
	 * @param workerId
	 * @param datacenterId
	 */
	private SnowFlakeKeyHandler(long workerId, long datacenterId) {
		if (workerId > maxWorkerId || workerId < 0) {
			throw new IllegalArgumentException(
					String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
		}
		if (datacenterId > maxDatacenterId || datacenterId < 0) {
			throw new IllegalArgumentException(
					String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
		}
		this.workerId = workerId;
		this.datacenterId = datacenterId;
	}

	/**
	 * 
	 * @return SnowflakeId
	 */
	public synchronized long nextId() {
		long timestamp = timeGen();

		/*
		 * If the current time is less than the timestamp generated by the
		 * previous ID, it means that the system clock should have thrown an
		 * exception at this time.
		 */
		if (timestamp < lastTimestamp) {
			throw new SnowFlakeException(String.format(
					"Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
		}

		/*
		 * If it is generated at the same time, the sequence is in milliseconds.
		 */
		if (lastTimestamp == timestamp) {
			sequence = (sequence + 1) & sequenceMask;
			/* The sequence overflows in milliseconds. */
			if (sequence == 0) {
				/* Block to the next millisecond and get a new timestamp. */
				timestamp = tilNextMillis(lastTimestamp);
			}
		}
		/* Timestamp change, the sequence reset in milliseconds. */
		else {
			sequence = 0L;
		}

		/* The time stamp that generated the ID last time. */
		lastTimestamp = timestamp;

		/* To form a 64 bit ID with or through a shift. */
		return ((timestamp - twepoch) << timestampLeftShift) //
				| (datacenterId << datacenterIdShift) //
				| (workerId << workerIdShift) //
				| sequence;
	}

	/**
	 * Block to the next millisecond until a new timestamp is obtained.
	 * 
	 * @param lastTimestamp
	 *            long
	 * @return Current timestamp
	 */
	protected long tilNextMillis(long lastTimestamp) {
		long timestamp = timeGen();
		while (timestamp <= lastTimestamp) {
			timestamp = timeGen();
		}
		return timestamp;
	}

	/**
	 * Returns the current time in milliseconds.
	 * 
	 * @return Current time (in milliseconds)
	 */
	protected long timeGen() {
		return System.currentTimeMillis();
	}

	@Override
	public String getKey() {
		return new Long(SnowFlakeKeyHandler.getInstance().nextId()).toString();
	}

}
